# PyOpenSG is (C) Copyright 2005-2009 by Allen Bierbaum
#
# This file is part of PyOpenSG.
#
# PyOpenSG is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# PyOpenSG is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
# more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import distutils.sysconfig
import os
import re
import types

try:
   import xml.etree.ElementTree as et
except:
   import elementtree.ElementTree as et

import xml.parsers.expat

import pygccxml
import pygccxml.declarations as pd
import pyplusplus.function_transformers as FT
import pyplusplus.module_builder.call_policies as cp

#import goodies.goodie_perf_overrides
from goodies import *


# ---------------------------------------------------------- #
#           Helper methods and Classes                       #
# ---------------------------------------------------------- #
class ClassInfo(object):
   def __init__(self, name, excludeList = None, policyList = None, includes = None,
                useSharedPtr = False, declarationCode = None, registrationCode = None,
                modCallable = None, finalize = False):
      if excludeList is None:
         excludeList = []
      if policyList is None:
         policyList = []
      if includes is None:
         includes = []
      if declarationCode is None:
         declarationCode = []
      if registrationCode is None:
         registrationCode = []

      self.name           = name
      self.excludes       = excludeList
      self.policies       = policyList
      self.includes       = includes
      self.use_shared_ptr = useSharedPtr
      self.declarations   = declarationCode
      self.registrations  = registrationCode
      self.modCallable    = modCallable         # Called as callable(class)
      self.finalize       = finalize

class FCInfo(object):
   def __init__(self, name, parent, excludeList = None, policyList = None, includes = None,
                hasBase = True, addMethods = None, templateMethods = None,
                templateInstances = None, documentation = ""):
      " If parent is None, then no parent. "
      if excludeList is None:
         excludeList = []
      if policyList is None:
         policyList = []
      if includes is None:
         includes = []
      if addMethods is None:
         addMethods = []
      if templateMethods is None:
         templateMethods = []
      if templateInstances is None:
         templateInstances = []

      self.name              = name
      self.parent            = parent
      self.excludes          = excludeList
      self.policies          = policyList
      self.includes          = includes
      self.hasBase           = hasBase
      self.addMethods        = addMethods
      self.templateMethods   = templateMethods    # A list of methods to treat as template methods
      self.templateInstances = templateInstances  # A list of methods to force instantiation of
      self.documentation     = documentation

def findFieldContainerDescriptors(fcInfoDict, srcDirRoot, srcSubdirs, pkgIncludeDir,
                                  fcIgnoreList = None, skipFcds = None):
   """
   Extends fcInfoDict with field containers discovered by searching the given source directory
   for .fcd files.

   @type  fcInfoDict:    dictionary mapping string to FCInfo object
   @param fcInfoDict:
   @type  srcDirRoot:    string
   @param srcDirRoot:
   @type  srcSubdirs:    list of strings
   @param srcSubdirs:
   @type  pkgIncludeDir: string
   @param pkgIncludeDir:
   @type  fcIgnoreList:  list of strings
   @param fcIgnoreList:
   @type  skipFcds:      list of strings
   @param skipFcds:
   """
   print "Building collection of field containers using discovered .fcd files..."
   fcd_files = []

   if fcIgnoreList is None:
      fcIgnoreList = []

   for subdir in srcSubdirs:
      for dirpath, _, files in os.walk(os.path.join(srcDirRoot, subdir)):
         if not "Depreciated" in dirpath:
            fcd_files += [os.path.join(dirpath, f) for f in files if f.endswith(".fcd")]

   # Filter out the bad .fcd files.
   if skipFcds:
      fcd_files = [fcd for fcd in fcd_files if fcd not in skipFcds]

   #print "found .fcd files:", fcd_files
   for f in fcd_files:
      #print "fcd:", f
      try:
         tree = et.parse(f)
         fc_elt = tree.getroot()
         assert fc_elt.tag == "FieldContainer"
         attribs = fc_elt.attrib.copy()
         parent = attribs["parent"]
         name = attribs["name"]
         doc = fc_elt.text.strip()
      except xml.parsers.expat.ExpatError, ex:
         #print "Error parsing file: %s"%f
         #print str(ex)
         #print "Trying re method instead. "

         file_text = file(f).read()
         name = re.search('name=\"(.*?)\"', file_text).group(1)
         parent = re.search('parent=\"(.*?)\"', file_text).group(1)
         library = re.search('library=\"(.*?)\"', file_text).group(1)
         doc = ""

      # Build up the class info
      #print "   name: %s  parent: %s" % (name, parent)
      if (not fcInfoDict.has_key(name) and name not in fcIgnoreList and
          os.path.exists(os.path.join(pkgIncludeDir, 'OSG%s.h' % name))):
         fcInfoDict[name] = FCInfo(name, parent, documentation = doc)

gCleanRe = re.compile('[\-:<>\s,]')
def cleanTemplateName(templateName):
   """
   Builds a clean template name.
   This is needed when we need a symbol name for code but only have a template name.
   """
   return gCleanRe.sub('_', templateName)

class LocalTemplateBuilder:
   """
   Template builder helper class.
   This class is meant to simplify the use of templates with Py++.

   It is a bit hairy, but the concept is that we try to:
   - Get some generated code into a file that will cause the template to instantiate
   - Create a typedef that we can use later to lookup the template instance

   XXX: Why do we need this instead of goodie_utils.TemplateBuilder???
   """
   def __init__(self):
      """ Initializes this template builder. """
      # List of tuples: (templateTypename, typedefName)
      #       ex: ("MyClass<float>","myclass_float")
      self.mTemplates = []

   def Template(self, templateType, typedefName = None):
      """
      Adds a template to instantiate.

      @param templateType: Fully instantiated name.  (ex: TemplateClass<float> )
      @param typedefName:  Name of type def to define to this template type. This name can then be
                           used later to look up the real fully expanded template type from the
                           typedef map.
      """
      if not typedefName:
         typedefName = cleanTemplateName(templateType)
      self.mTemplates.append( (templateType, typedefName) )

   def buildTemplateFileContents(self):
      """
      Builds up the contents of a file to instantiate the needed templates.
      """
      if len(self.mTemplates) == 0:
         return None

      content = "/** Autogenerated temporary file for template instantiation. */\n"
      for t in self.mTemplates:
         template_type = t[0]
         typedef_name = t[1]
         content += """
            typedef %(template_type)s %(typedef_name)s;
            inline unsigned __instantiate_%(typedef_name)s()
            { return unsigned(sizeof(%(typedef_name)s)); }
         """ % vars()

      return content

def buildContainerHeader(headers, extraContent = "/** Nothing Extra */", filename = None):
   """
   Builds up a header containing a bunch of other headers.

   @param headers: List of header files for this module.

   @return: returns a temp file or a file created with filename.
   """
   if filename == None:
      filename = pygccxml.utils.create_temp_file_name(suffix = ".h")
   content = "/** Auto-generated temporary file for header containment. */\n"
   for h in headers:
      content += '#include <%s>\n'%h

   # This bit is to deal with the cold, hard reality that X11 defines Bool as a preprocessor
   # symbol. That symbol interferes with Boost.
   content += "\n#if defined(Bool)\n#undef Bool\n#endif"

   content += '\n\n'
   content += extraContent
   content += '\n\n'

   temp_file = file(filename, 'w')
   temp_file.write(content)
   temp_file.close()

   return filename

class FCPolicyResolver(object):
   """
   Call policy resolver for field containers and OpenSG classes. Based on default_policy_resolver
   and return_value_policy_resolver. Tries to do as much automatically as possible to wrap OpenSG
   methods.

   Take a look at default_policy_resolver and return_value_policy_resolver for inspiration,
   details, and ideas to make this better.
   """
   sGetSF_re  = re.compile('getSF.*')
   sGetMF_re  = re.compile('getMF.*')
   sGet_re    = re.compile('get.*')
   sEdit_re   = re.compile('edit.*')
   sCreate_re = re.compile('create.*')

   def __init__(self):
      self.__const_char_pointer = pointer_t(const_t(char_t()))
      self.__const_wchar_pointer = pointer_t(const_t(wchar_t()))

   def __call__(self, calldef, hint = None):
      if not isinstance( calldef, calldef_t ):
         return None

      if isinstance(calldef, constructor_t):
         return None

      # Build up helper variables
      ret_type      = remove_alias( calldef.return_type )
      naked_type    = remove_cv(remove_pointer(remove_reference(ret_type)))
      rt_is_const   = \
         is_const(ret_type) or (hasattr(ret_type, 'base') and is_const(ret_type.base))
      rt_is_ref     = \
         is_reference(ret_type) or (hasattr(ret_type, 'base') and is_reference(ret_type.base))
      rt_is_pointer = is_pointer(ret_type)
      rt_is_class   = is_class(naked_type)
      rt_is_fundamental = isinstance(naked_type, fundamental_t)

      #print "method: [%s] is_ref:%s is_const:%s is_pointer:%s is_class:%s"%\
      #        (calldef.name, rt_is_ref, rt_is_const, rt_is_pointer, rt_is_class)
      #print_declarations(calldef, detailed = True, recursive = True)

      # Custom methods
      if calldef.name in ["getType", "getClass", "getStaticClass"]:
         return return_internal_reference()

      # getClassType() is a static method that returns a reference to an
      # existing object. Because it is a static method, it cannot use
      # return_internal_reference because there is no custodian object.
      if calldef.name in ["getClassType"]:
         return return_value_policy(reference_existing_object)

      # Functions that return raw pointers. For example, creation methods
      # return new objects.
      # - Only something special when returning ptr, smart_ptr handled already
      if rt_is_pointer:
         if FCPolicyResolver.sCreate_re.match(calldef.name):
            return return_value_policy(manage_new_object)
         elif (FCPolicyResolver.sGetMF_re.match(calldef.name) or
               FCPolicyResolver.sGetSF_re.match(calldef.name)):
            if rt_is_fundamental:
               return return_value_policy(return_by_value)
            else:
               return return_internal_reference()

      # Handle singleton method
      if "the" == calldef.name:
         return return_value_policy(reference_existing_object)

      # Check for default cases of:
      # - const_char_pointer, wchar ptr
      # - fundamental type (ref and non ref
      if is_same( naked_type, self.__const_char_pointer ):
         return default_call_policies()
      if is_same( ret_type, self.__const_wchar_pointer ):
         return return_value_policy( return_by_value )
      if rt_is_fundamental:
         if rt_is_ref:
            return return_value_policy(return_by_value)
         else:
            return default_call_policies()

      #if isinstance( ret_type, declarations.reference_t ) \
      #   and isinstance( ret_type.base, declarations.const_t ):
      if rt_is_const and rt_is_ref and rt_is_class:
         return return_value_policy( copy_const_reference )

      # Check gets, edits, and [] for things returning internal references
      # Note: Why not just do this on all return types?  Well because we don't
      #       know for sure that we can do this.  It is method specific and we are just
      #       making assumptions based on naming conventions in OpenSG
      if (FCPolicyResolver.sGet_re.match(calldef.name) or
          FCPolicyResolver.sEdit_re.match(calldef.name) or
          (hasattr(calldef, 'symbol') and calldef.symbol == '[]')):
         if rt_is_ref:
            if is_fundamental( naked_type ) or is_enum( naked_type ):
               if is_const:
                  return return_value_policy(copy_const_reference)
               else:
                  return return_value_policy(copy_non_const_reference)
            elif rt_is_const:
               return return_value_policy(copy_const_reference)
            else:
               return return_internal_reference()
         elif rt_is_pointer:
            if rt_is_fundamental:
               assert false, "should not get here"
               return default_call_policies()
            elif rt_is_const:
               return default_call_policies()
            elif isinstance(calldef, pd.member_function_t):
               return return_internal_reference()
            else:
               return return_value_policy(reference_existing_object)

      # Handle Image:: returning const *. For now try this in general
      if rt_is_const and rt_is_pointer:
         return return_internal_reference()

      return None

# The FCPolicyResolver instance to use
FC_POLICY_RESOLVER = FCPolicyResolver()

def excludeMembers(cls, excludeList, willFinalize = True):
   # XXX: The following handling of the exportable members and redefined functions is to deal with
   # an unexpected case where one or both end up being a list containing a single element that is
   # the desired list. There is probably a reason why this occurs, but it is not obvious.
   exportable_mems = cls.get_exportable_members()
   if exportable_mems is None:
      exportable_mems = []
   elif exportable_mems and type(exportable_mems[0]) is types.ListType:
      exportable_mems = exportable_mems[0]

   redefined_funcs = cls.redefined_funcs()
   if redefined_funcs is None:
      redefined_funcs = []
   elif redefined_funcs and type(redefined_funcs[0]) is types.ListType:
      redefined_funcs = redefined_funcs[0]

   # The following approach is used so that abstract classes that inherit a pure virtual method
   # but do not override it can have such a method excluded properly. Otherwise, methods such as
   # OSG::CameraDecorator::shallowCopy() could not be excluded, thus preventing that class from
   # being finalized later.
   for member in exportable_mems + redefined_funcs:
      if isinstance(member, pd.calldef_t) or isinstance(member, pd.variable_t):
         if member.name in excludeList:
            member.exclude()

            # Taking this step is done to prevent Py++ from generating a class wrapper when one is
            # not wanted. If we exclude a pure virtual method, we mean it. Py++ interprets the
            # presence of pure virtual methods as a signal to create a class wrapper even if the
            # method is excluded.
            # NOTE: Abstract classes will be exposed using boost::python::no_init, so there is no
            #       need for us to worry about attempts to instantiate such types.
            if (willFinalize and hasattr(member, "virtuality") and
                member.virtuality == pd.VIRTUALITY_TYPES.PURE_VIRTUAL):
               member.set_virtuality(pd.VIRTUALITY_TYPES.NOT_VIRTUAL)

gRetMF_re = re.compile('.*MF.*')

def handleClassDefaults(cls, willFinalize):
   """
   Performs default processing on the given class. This includes just about everything we can do
   by default to change methods exposed, call policies, etc.
   """
   exclude_protected(cls)
   wrap_const_ref_params(cls)

   # Hide all Mask variables. These don't expose easily, although if we need them, we can get
   # them to be exposed correctly.
   cls.variables(regex_matcher(".*Mask"), allow_empty = True).exclude()
   cls.variables(allow_empty = True).exclude()
   cls.variables(regex_matcher("stat.*"), allow_empty = True).include()
   cls.variables(regex_matcher("stat.*"), allow_empty = True).is_read_only = True

   # For now ignore all the field based access and the edit based ref access.
   cls.member_functions(regex_matcher("edit.*"), allow_empty = True).exclude()
   cls.member_functions(regex_matcher("edit[MS]F.*"), allow_empty = True).exclude()

   # Remove a set of methods and parameters that we have deemed to be "bad", (ie. difficult or
   # impossible to wrap).
   common_exclude = ["isNodeCore", "newPtr", "editSField", "editMField",
                     "pushToField", "insertIntoMField", "replaceInMField",
                     "replaceInMField", "removeFromMField", "removeFromMField",
                     "clearField", "createEmpty", "createEmptyLocal",
                     "registerChangedContainer", "registerChangedContainerV",
                     "execSync", "execSyncV", "deregister",
                     "getData", "editData"]
   # Methods that we don't want to expose for now because we can't or don't use them.
   dont_expose = ["draw", "activate", "deactivate", "changeFrom", "shallowCopy",
                  "shallowCopyLocal", "shallowCopyDependent", "createAspectCopy"]

   excludeMembers(cls, common_exclude + dont_expose, willFinalize)

   # Build up a list of unique method names from the class.
   all_calldefs = cls.calldefs()
   contained_names = set()
   for c in all_calldefs:
      contained_names.add(c.name)

   for n in contained_names:
      #print "  checking calldef: [%s]"%n,
      methods = cls.calldefs(n)
      #print "  found num: ", len(methods)

      # Resolve the call policies on any methods left.
      for m in methods:
         # Skip the method if it is ignored.
         if True == m.ignore:
            continue

         # These operations on chunks, materials, images cause problems with Boost.Python because
         # of protected destructor.
         bad_ops = ["<", "==", "!="]
         bad_ops.extend(["->"])      # We just don't want these ops because they don't help
         if isinstance(m, operator_t) and m.symbol in bad_ops:
            m.exclude()
            continue

         m.call_policies = FC_POLICY_RESOLVER(m)

   # Overrides.
   for c in all_calldefs:
      ret_type = remove_alias( c.return_type )
      ret_naked_type = remove_cv(remove_pointer(remove_reference(ret_type)))
      rt_is_const = is_const(ret_type) or (hasattr(ret_type, 'base') and is_const(ret_type.base))
      rt_is_ref = \
         is_reference(ret_type) or (hasattr(ret_type, 'base') and is_reference(ret_type.base))
      rt_is_pointer = is_pointer(ret_type)
      rt_is_class = is_class(ret_naked_type)
      rt_is_mf_type = rt_is_class and gRetMF_re.match(ret_naked_type.declaration.name)

      if not c.ignore and rt_is_mf_type and rt_is_const:
         # Write custom wrapper for the return value
         addMFRetListWrapper(c, ret_naked_type, rt_is_pointer)

def addMFRetListWrapper(calldef, nakedRetType, mfIsPointer):
   """
   Adds a custom wrapper for methods that return data through an MField. The wrapper changes the
   method return type to be a Python list of the field data.

   @type  calldef:
   @param calldef:      The declaration of the method that will be wrapped.
   @type  nakedRetType:
   @param nakedRetType: The undecorated return type. This has had cv-qualification, pointer, and
                        reference information stripped from it.
   @type  mfIsPointer:  boolean
   @param mfIsPointer:  Identifies whether the MField object returned by the method is a pointer.
   """
   class_decl = calldef.parent
   classname = class_decl.name
   fullclassname = class_decl.decl_string.lstrip(":")
   methodname = calldef.name

   # We use the actual return type from the method being invoked rather than
   # the "naked" return type. In general, this avoids copying the MField
   # object, and it allows us to use the typedef'd MField name quite easily.
   # This is important for portability reasons (in particular because GLenum,
   # as used by OSG::MFGLenum) is defined differently on different platforms.
   mftype = calldef.return_type.decl_string.lstrip(":")

   new_methodname = "%s_%s"%(cleanTemplateName(classname), methodname)   # calldef.location.line

   if mfIsPointer:
      get_size  = "mf_data->size()"
      get_entry = "(*mf_data)[i]"
   else:
      get_size  = "mf_data.size()"
      get_entry = "mf_data[i]"

   code = """
boost::python::list %(new_methodname)s(%(fullclassname)s *self)
{
   boost::python::list result;
   %(mftype)s mf_data = self->%(methodname)s();
   const OSG::UInt32 size(%(get_size)s);
   for ( OSG::UInt32 i = 0; i < size; ++i )
   {
      result.append(%(get_entry)s);
   }
   return result;
}""" % vars()

   class_decl.add_declaration_code(code)
   calldef.ignore = True
   #print "Adding MFRetList wrapper for: %s %s"%(classname, methodname)
   add_member_function(class_decl, methodname, new_methodname)

def addFCPtrCode(c, mb, className, parentName = None):
   """
   For a given class, add all the appropriate pointer handling code we need to wrap.

   @type  c:          pyplusplus.decl_wrappers.class_wrapper.class_t object
   @param c:          The class object to be modified.
   @type  mb:         goodies.ModuleBuilder object
   @param mb:         Our module builder.
   @type  className:  string
   @param className:  The name of the class we are dealing with.
   @type  parentName: string
   @param parentName: The name of the parent class of className or None if the class has
                      no parent.
   """
   # Some class names come in to this function with the OSG:: scope already in place. Most do not,
   # so we just strip it off in the event that we encounter that case.
   if className.startswith("OSG::"):
      className = className.replace("OSG::", "")
   if parentName and parentName.startswith("OSG::"):
      parentName = parentName.replace("OSG::", "")

   # If we are noncopyable, then we can't set a held type
   # but we still need to register the pointer so python knows about us
   ptr_str = "OSG::%s::ObjCPtr" % className
   #const_ptr_str = "OSG::%s::ConstObjCPtr" % className
   rec_ptr_str = "OSG::%s::ObjRecPtr" % className
   if not c.is_abstract:
      c.held_type = rec_ptr_str

   c.add_registration_code("pyopensg::register_transit< OSG::%s >::execute();" % className,
                           works_on_instance = False)

   for ptr_type in ["ObjRecPtr"]: #, "ObjUnrecPtr", "ObjWeakPtr"]:
      other_ptr_str = "OSG::%s::%s" % (className, ptr_type)
      if parentName:
         other_ptr_parent_str = "OSG::%s::%s" % (parentName, ptr_type)

      if c.is_abstract:
         #print "XXXXXXXXXXXXXX: Abstract: %s XXXXXXXXXXXXXXXXXXXXXXXXXXX"%className
         c.add_registration_code("bp::register_ptr_to_python< %s >();" % other_ptr_str,
                                 works_on_instance = False)

      # Add on the ptr conversion methods needed
      c.add_registration_code(
         "bp::implicitly_convertible< %s, OSG::%s* >();" % (other_ptr_str, className),
         works_on_instance = False
      )
      c.add_registration_code("bp::implicitly_convertible< %s, %s >();" % (other_ptr_str, ptr_str),
                              works_on_instance = False)
      if parentName:
         c.add_registration_code("bp::implicitly_convertible< %s, OSG::%s* >();" % \
                                    (other_ptr_str, parentName),
                                 works_on_instance = False)
         c.add_registration_code("bp::implicitly_convertible< %s, %s >();" % \
                                    (other_ptr_str, other_ptr_parent_str),
                                 works_on_instance = False)

   if parentName:
      c.add_registration_code("bp::implicitly_convertible<%s, OSG::%s::ObjCPtr>();" % \
                                 (rec_ptr_str, parentName),
                              works_on_instance = False)

   mb.add_registration_code("/** Helpers for OSG::%s */" % className)
   mb.add_registration_code('bp::def("RecPtr", &pyopensg::ToRecPtr<OSG::%s >);' % className)
   mb.add_registration_code('bp::def("CPtr", &pyopensg::ToCPtr<%s >, ' % other_ptr_str + \
                            'bp::return_value_policy<bp::reference_existing_object>());')

def exposeTemplateInst(cls, newName = None):
   cls.include()
   if newName is not None:
      cls.rename(newName)

def exposeSingleton(cls, newName = None):
   """ Includes the class, rename it to a new name. """
   exposeTemplateInst(cls, newName)
   cls["the"].call_policies = return_value_policy(reference_existing_object)
   cls["_the"].exclude()

def removePureVirtuals(cls):
   """
   Helper function to remove pure virtuals from class methods. This sets them to virtual so that
   boost::python::pure_virtual is not used but they still get the rest of the wrapper support.
   """
   pure_members = cls.decls(virtuality_type_matcher(pd.VIRTUALITY_TYPES.PURE_VIRTUAL),
                            decl_type = pd.member_calldef_t, allow_empty = True)
   pure_members.set_virtuality(pd.VIRTUALITY_TYPES.VIRTUAL)

def addStrOperator(cls):
   """ Adds a simple def that uses op<<. """
   cls.add_registration_code('def(boost::python::self_ns::str(boost::python::self))', True)

class UnlockedNative(FT.transformer_t):
   def __init__(self, function):
      FT.transformer_t.__init__(self, function)

   def __str__(self):
      return "unlocked_native()"

   def required_headers(self):
      return []

   def __configureSealed(self, controller):
      # Create a guard for the GIL before the actual fucntion call is made.
      # This is an exception-safe approach to managing the GIL.
      controller.add_pre_call_code('osgwrap::ReleaseGILGuard guard();')

   def __configureDefault(self, controller):
      self.__configureSealed(controller)

   def configure_free_fun(self, controller):
      self.__configureSealed(controller)

   def configure_mem_fun(self, controller):
      self.__configureSealed(controller)

   def configure_virtual_mem_fun(self, controller):
      # For a virtual function, we only have to handle the defaualt call
      # case. A Python override of the C++ method needs to continue holding
      # the GIL.
      self.__configureDefault(controller.default_controller)

def unlocked_native():
   '''
   Returns a function transform creator for native functions and methods that
   do not hold the GIL while they execute.
   '''
   def creator(function):
      return UnlockedNative(function)

   return creator
